package featuredb

import (
	"crypto/md5"
	"sort"
	"time"

	"github.com/boltdb/bolt"
	"github.com/getkin/kin-openapi/openapi3"
	"github.com/sirupsen/logrus"
	"google.golang.org/protobuf/proto"
)

var localDb *DB

var (
	Prefix_Url         byte = 1 //SOH
	Prefix_Method      byte = 2 //STX
	FeatureDBStatTable      = []byte("_apicat_FeatureDBStatTable")
)

// a random seperator
var sepOfKey = []byte{0x4d, 0x24, 0x1f}
var sepOfValue = []byte{0xed, 0x2a, 0x6f}

//特征数据库采用本地存储的形式进行增量式记录，以便形成url特性判断的长期数据库
//长期数据库支持通过对日志的重复记录实现加强训练
//同时也支持清空等操作
//TODO 报告功能应对基础数据库清空进行说明，包括训练数据量等
//TODO 拦截功能可配置对不稳定条目推迟反应
//TODO 特性报告也应记录部分错误访问的特性，以便在判定时快速做出反应

type DB struct {
	low               *bolt.DB
	ipKeySize         int
	inMemoryHotIPStat IPStatPool
	syncChan          chan bool
}

//后台执行的同步任务，向数据库中写入内存中的统计信息
func (d *DB) sync() {
	d.low.Update(func(tx *bolt.Tx) error {
		bucket := tx.Bucket(FeatureDBStatTable)
		for k, v := range d.inMemoryHotIPStat {
			bts, err := proto.Marshal(v)
			if err == nil {
				bucket.Put([]byte(k), bts)
			}
		}
		return nil
	})
}

func (d *DB) Stat(pName, mName string) *StatWrapper {
	var stat *Stat
	var key = getKeyFromPathAndMethod(pName, mName)
	var err error

	if d.inMemoryHotIPStat == nil {
		d.inMemoryHotIPStat = make(IPStatPool)
	}
	stat, ok := d.inMemoryHotIPStat[string(key)]
	if !ok {
		err := d.low.View(func(tx *bolt.Tx) error {
			bucket := tx.Bucket(FeatureDBStatTable)
			if bucket != nil {
				bts := bucket.Get(key)
				if len(bts) > 0 {
					stat = newStat()
					return proto.Unmarshal(bts, stat)
				}
			}
			return nil
		})
		if err == nil {
			if stat == nil {
				stat = newStat()
				err = d.low.Update(func(tx *bolt.Tx) error {
					bucket, err := tx.CreateBucketIfNotExists(FeatureDBStatTable)
					if err == nil {
						var bts []byte
						bts, err = proto.Marshal(stat)
						if err == nil {
							err = bucket.Put(key, bts)
						}
					}
					return err
				})
			}
		}
		if err != nil {
			goto handleErr
		}
		d.inMemoryHotIPStat[string(key)] = stat
	}
	return &StatWrapper{
		stat: stat,
		db:   d,
		key:  key,
		// inMemoryHotIPStat: d.inMemoryHotIPStat,
	}
handleErr:
	logrus.Fatal("日志写入出错", err)
	return nil
}

func (d *DB) VarsToKey(vars map[string]string) int64 {
	//order the vars
	var keys = make([]string, 0, len(vars))
	for key := range vars {
		keys = append(keys, key)
	}
	//sort the keys
	sort.Strings(keys)
	//build the key
	var keyResult []byte
	for _, key := range keys {
		var value = vars[key]
		//append the key
		keyResult = append(keyResult, sepOfKey...)
		keyResult = append(keyResult, []byte(key)...)
		keyResult = append(keyResult, sepOfValue...)
		keyResult = append(keyResult, []byte(value)...)
	}
	//md5 the key, 参数MD5采用默认长度
	return d.bytesToMd5(keyResult, 32)
}

//ip转换为key
//TODO 需要考虑ip比较多，因此需要缩减key的长度
func (d *DB) IPToKey(ip string) int64 {
	return d.bytesToMd5([]byte(ip), d.ipKeySize)
}

//为了保证key的唯一性，需要对key进行md5处理
func (d *DB) bytesToMd5(bts []byte, length int) int64 {
	sum := md5.New()
	sum.Write(bts)
	keys := sum.Sum(nil)

	//截取指定长度
	for i := 32; i > length; i++ {
		keys[i-1] = 0
	}
	//bytes to int64-
	var result int64
	for i := 0; i < 8; i++ {
		result = result<<8 + int64(keys[i])
	}
	return result
}

func (d *DB) applySync() {
	if d.syncChan != nil {
		d.syncChan <- true
	} else {
		d.syncChan = make(chan bool, 1)
	}
	select {
	case <-time.After(time.Second * 10):
		d.sync()
		d.syncChan = nil
	case <-d.syncChan:
		return
	}
}

//GetStatForAPI 根据定义从统计库中获得相关统计
func GetStatForAPI(path, mName string, def *openapi3.Operation) StatInDb {
	go localDb.applySync()
	return localDb.Stat(path, mName)
}

func getKeyFromPathAndMethod(pName, mName string) []byte {
	var bytes = make([]byte, len(pName)+len(mName)+2)
	bytes[0] = Prefix_Url
	copy(bytes[1:], []byte(pName))
	var startOfMethod = 1 + len(pName)
	bytes[1+len(pName)] = Prefix_Method
	copy(bytes[startOfMethod+1:], []byte(mName))
	return bytes
}

func Init(path string, size int) error {
	db, err := bolt.Open(path, 0600, nil)
	if err == nil {
		InitByDb(db, size)
	}
	return err
}

func InitByDb(db *bolt.DB, size int) {
	localDb = &DB{
		low:               db,
		ipKeySize:         size,
		inMemoryHotIPStat: make(IPStatPool),
	}
}

func newStat() *Stat {
	return &Stat{
		StatPerVars: make(map[int64]*VarStat),
		SizeOfIPs:   make(map[int64]int64),
	}
}
